package jass.generators;
import jass.engine.*;
import java.io.*;
import java.util.*;

/** UG that maintains a list of sources and QuenchableModalObjectWithOneContact's and
    estimates excitations and turns off inaudible modes according to a masking analysis.
    @author Kees van den Doel (kvdoel@cs.ubc.ca)
*/
public class ModalQuencher extends InOut {

    /** This is the level in dB SPL of loudest mode (as hear by listener)
        Should be estimated with a mike, if possible. Here we just assume 60 dB
        as a "reference" level.
     */
    protected float dbLevelLoudestMode = 60;

    /** Masking curve offset (how high about masker) */
    protected float av = 20;

    /** maximum excitation occurred */
    protected float maximumExdBThatOccurred = -100000;
    
    /** Sampling rate in Hertz. */
    public float srate;

    /** Registered QuenchableModalObjectWithOneContact's */
    private Vector<QuenchableModalObjectWithOneContact> modalObjectsContainer;

    /** Registered QuenchableModalObjectWithOneContact's */
    protected QuenchableModalObjectWithOneContact[] mobs;

    /** Estimated excitations of inputs to the above QuenchableModalObjectWithOneContact */
    protected float[] sourceExcitations;

    /** Modal data array */
    protected ModeData[] modeData;

    /** How many frames to skip before quenching again */
    protected int nFramesToSkip = 1;

    private int nMobs = 0;

    /** For monitoring purpose */
    protected  int nKilledModes = 0;
    protected int nTotalModes = 0;

    /**
       @return number of modes that where pruned
     */
    public int getNKilledModes() {
        return nKilledModes;
    }

    /**
       @return total number of modes
     */
    public int getTotalModes() {
        return nTotalModes;
    }
    
    /**
       Constructor.
       @param bufferSize internal buffersize
       @param nFramesToSkip how many frames to skip before quenching again
     */
    public ModalQuencher(int bufferSize, int nFramesToSkip) {
        super(bufferSize);
        setFramesToSkip(nFramesToSkip);
        modalObjectsContainer = new Vector<QuenchableModalObjectWithOneContact>();
    }

    /** To be called after all sources and mobs have been added */
    public void init() {
        mobs = new QuenchableModalObjectWithOneContact[nMobs];
        for(int i=0;i<nMobs;i++) {
            mobs[i] = modalObjectsContainer.get(i);
        }
        makeModeDataStructure();
    }

    /**
       Set masking curve height.
       @param val offset in dB (20 is safe, 1 is agressive)
     */
    public void setAv(float val) {
        av = val;
    }

    /**
       Set level in dB SPL at observer of loudest mode. This will be the loudest value during a run.
       @param val loudest mode at loudest moment in dB
     */
    public void setDbLevelLoudestMode(float val) {
        dbLevelLoudestMode = val;
    }
    
    /**
       Reset level
     */
    public void resetLevel() {
        maximumExdBThatOccurred = 0;
    }
    
    /**
       How many frames (buffers) to skip before recomputing modal quenching
       @param n nFramesToSkip
     */
    public void setFramesToSkip(int n) {
        nFramesToSkip = n;

    }
    /**
       When this is called, all sources have already been queried (available through
       getSources(). Estimate excitations from sources for each
       QuenchableModalObjectWithOneContact, enter them in ModeData[], then
       turn on/off appropriate modes.
     */
    protected void computeBuffer() {
        if(getTime()%nFramesToSkip == 0) {
            doQuenching();
        }
    }

    /**
       Sort estimate excitations, sort ModeData[], quench
    */
    protected void doQuenching() {
        estimateExcitations();
        sortModeData();
        quench();
    }

    // estimate levels of inputs
    private void getSourceLevels() {
        Object[] arrayOfSources = getSources();
        for(int is=0;is<nMobs;is++) {
            sourceExcitations[is] = 0;
            Out currentSource = (Out) arrayOfSources[is];
            float[] sourceBuffer = currentSource.peekAtBuffer();
            for(int i=0;i<bufferSize;i++) {
                sourceExcitations[is] += sourceBuffer[i] * sourceBuffer[i];
            }
//            System.out.println("exc["+is+"] = "+ sourceExcitations[is]);
        }
    }

    /**
       Estimate exitations. Load     protected float[] sourceExcitations
       with sum_i sourceBuf^2[i], for the source buffers.
     */
    protected void estimateExcitations() {
        getSourceLevels();
        int totalnmodes = modeData.length;
        int point = 0; // works only for 1 location
        for(int i=0;i<totalnmodes;i++) {
            ModeData md = modeData[i];
            QuenchableModalObjectWithOneContact qmob = md.parent;
            int indexOfModeInQMOB = md.indexInParent;
            float currentExcitation = qmob.getModeExcitation(indexOfModeInQMOB);
            float a_mode = qmob.modalModel.a[point][indexOfModeInQMOB];
            float sourceExcitation = a_mode *  sourceExcitations[md.indexOfModalObject];
            // set excitation
            md.ex = currentExcitation + sourceExcitation;
            md.exdB = BarkScale.decibel(md.ex);
            md.exMOBdB = BarkScale.decibel(currentExcitation);
            // use exMOB as we use this to set the masker level
            if(md.exMOBdB > maximumExdBThatOccurred) {
                maximumExdBThatOccurred = md.exdB;
                //System.out.println("max level = " + maximumExdBThatOccurred);
            }
        }
        
    }

    /**
     */
    protected void quench() {
        MaskingCurve.av = this.av;
        // first set all flag to on
        int ntotalmodes = modeData.length;
        for(int i=0;i<ntotalmodes;i++) {
            modeData[i].on = true;
            //float lev = modeData[i].exdB+ dbLevelLoudestMode - maximumExdBThatOccurred;
            //System.out.println("ex["+i+"] = " + lev);
        }
        // add this to all excitations to normalize absolute levels to the
        // actual level (which may be true or "reference"). I.e., we
        // pretend maximumExdBThatOccurred = dbLevelLoudestMode
        float addToEx = dbLevelLoudestMode - maximumExdBThatOccurred;
        // for every mode, starting with loudest kill all modes it masks. If lies below threshold kill self and continue
        for(int i=0;i<ntotalmodes-1;i++) {
            if(modeData[i].on == true) { // skip off ones
                float levelMasker = modeData[i].exMOBdB + addToEx;
                if((modeData[i].exdB + addToEx) <  modeData[i].ath) {
                    modeData[i].on = false;
                } else {
                    float bMasker = modeData[i].b;
                    for(int k=i+1;k<ntotalmodes;k++) {
                        if(modeData[k].on == true) { // skip off ones
                            float levelMasked =  modeData[k].exdB + addToEx;
                            if(levelMasked <  modeData[i].ath) {
                                modeData[k].on = false;
                            } else {
                                float bMaskee = modeData[k].b;
                                if(levelMasked < MaskingCurve.masker(bMaskee,bMasker,levelMasker)) {
                                    modeData[k].on = false;
                                }
                            }
                        }
                    }
                }
            }
        }
        // loaded ModeData[], now kill modes
        int nkilled=0;
        for(int i=0;i<ntotalmodes;i++) {
            modeData[i].parent.setOnBit(modeData[i].indexInParent,modeData[i].on);
            if(!modeData[i].on) {
                nkilled++;
            }
        }
        double perc = 100*(ntotalmodes-nkilled)/((double)ntotalmodes);
        //System.out.println("killed: "+nkilled +"/"+ntotalmodes+" using: "+perc+"%");
        this.nKilledModes = nkilled;
        this.nTotalModes = ntotalmodes;
    }
    
    /** Add QuenchableModalObjectWithOneContact to Sink.
        @param s QuenchableModalObjectWithOneContact to add.
        @return object representing Source in Sink (may be null).
    */
    public synchronized void addModalObject(QuenchableModalObjectWithOneContact s) {
        modalObjectsContainer.addElement(s);
        nMobs++;
    }

    /** Get array of QuenchableModalObjectWithOneContact's
        @return array of QuenchableModalObjectWithOneContact's, null if there are none.
    */
    public QuenchableModalObjectWithOneContact[] getModalObjects() {
        return modalObjectsContainer.toArray(new QuenchableModalObjectWithOneContact[0]);
    }

    protected void sortModeData() {
        QSort.sort(modeData);
    }

    
    /** Make data structure (to be called after all Sources and
        QuenchableModalObjectWithOneContact's have been added).
        Also allocate the array of excitations. Each source is attached to 1 ModalObject.
    */
    private void makeModeDataStructure() {
        int nmodes = 0;
        for(int i=0;i<mobs.length;i++) {
            nmodes += mobs[i].modalModel.nfUsed;
        }
        modeData = new ModeData[nmodes];
        for(int i =0;i<nmodes;i++) {
            modeData[i] = new ModeData();
        }
        sourceExcitations = new float[mobs.length];
        int imode = 0;
        for(int i=0;i<mobs.length;i++) {
            sourceExcitations[i] = 0;
            ModalModel mm = mobs[i].modalModel;
            int nfInHere = mm.nfUsed;
            for(int k=0;k<nfInHere;k++) {
                modeData[imode].f = mm.f[k] * mm.fscale;
                modeData[imode].b = BarkScale.bark(mm.f[k]);
                modeData[imode].d = mm.d[k] * mm.dscale;
                modeData[imode].a = mm.a[0][k] * mm.ascale;
                if(modeData[imode].d != 0) {
                    modeData[imode].a2_d =
                        modeData[imode].a * modeData[imode].a / modeData[imode].d;
                } else {
                    modeData[imode].a2_d = 0;
                }
                modeData[imode].ath = 0; // simple model for this for now
                modeData[imode].ex = 0;
                modeData[imode].exdB = BarkScale.decibel(modeData[imode].ex);
                modeData[imode].exMOBdB = BarkScale.decibel(0);
                modeData[imode].parent = mobs[i];
                modeData[imode].indexOfModalObject = i;
                modeData[imode].indexInParent = k;
                modeData[imode].on = true;
                imode++;
            }
        }
    }

}

class ModeData implements Comparable {
    float f; // freq in Hz
    float b; //freq in Barks
    float d; // damping in 1/s
    float a; // gain
    float a2_d; // a^2/d
    float ath; // absolute threshold for this mode (in terms of y^2, y audio signal)
    float ex; // current excitation (energy)
    float exdB; // current excitation in dB (10log_10(energy))
    float exMOBdB; // current excitation of ModalObject only (not including source) in dB (10log_10(energy))
    QuenchableModalObjectWithOneContact parent; // ModalObject it belongs to
    int indexOfModalObject; // 0,.., n-1 for n objects, refers to order in patch
    int indexInParent; // index of mode in parent
    boolean on; // true if this mode is on, false otherwise

    public int compareTo(Object o) {
        ModeData m = (ModeData)o;
        if(this.ex < m.ex) {
            return 1;
        } else if(this.ex > m.ex) {
            return -1;
            // this.ex == m.ex:
        } else if(this.a2_d < m.a2_d) {
            return 1;
        } else if(this.a2_d > m.a2_d) {
            return -1;
            // this.a2_d == m.a2_d
        } else if(this.f < m.f) {
            return 1;
        } else if(this.f > m.f) {
            return -1;
        } else {
            return 0;
        }
    }
}

// IDEA: since excitation is overestimated, may have 2 much masking, so correct for this
// by using only the ACTUAL excitation in ModalObject to set masker, but use SUM
// for maskees

/** Defines masking curve for source at given freq. in Barks. */
class MaskingCurve {
    
    static private final float sl = 25; // lower slope; constant

    /** magic number defining level of masker.
        20dB suggested by literature, experimentally 65dB is OK
    */
    static public float av = 5; 

    /** float lm = level in dB SPL of source */
    static private final float su(float lm) {
        return (float)(22 - lm/5); // upper slope
    }

    /**
       Masking curve (function of maskee bMaskee in Barks) for masker of level
       lm (dB SPL) and Bark bMasker.
       @param bMaskee freq. in Barks of masked freq.
       @param bMasker freq. in Barks of masking freq.
       @param lm dB SPL loudness of masker
       @return dB SPL value of masking curve
     */
    public static final float masker(float bMaskee, float bMasker, float lm) {
        if(bMaskee < bMasker) {
            return (float)(lm - av + sl * (bMaskee - bMasker));
        } else {
            return (float)(lm - av + su(lm) * (bMasker - bMaskee));
        }
    }
}

/** Convert to and from Barkscale
    Use:    Schroeder (1977): bark = 7*asinh(f/650);
*/
class BarkScale {

    static private final double  FACTOR = 10/Math.log(10);
    static private final double  MIN = 1.e-10;

    static private final double asinh(double x) {
        return Math.log(Math.sqrt(x*x+1) + x);
    }
    
    static private final double sinh(double x) {
        return (Math.exp(x) - Math.exp(-x))/2;
    }
    
    static public final float bark(float hertz) {
        return (float)(7*asinh((float)(hertz/650)));
    }
    
    static public final float hertz(float bark) {
        return (float)(650 * sinh(bark/7));
    }

    static public final float decibel(float energy) {
        return (float)(FACTOR * Math.log(Math.max(MIN,Math.abs(energy))));
    }
}

class QSort  {
    
    static final public void  sort(ModeData[] list) {
//        System.out.println("sorting "+list.length +" items");
        quicksort(list, 0, list.length-1);
    }
    
    static final private void quicksort(ModeData[] list, int p, int r) {
        if (p < r) {
            int q = partition(list,p,r);
            if(q == r) {
                q--;
            }
            quicksort(list,p,q);
            quicksort(list,q+1,r);
        }
    }
    
    static final private int partition (ModeData[] list, int p, int r) {
        ModeData pivot = list[p];
        int lo = p;
        int hi = r;
        
        while (true) {
            while (list[hi].compareTo(pivot) >= 0 && lo < hi) {
                hi--;
            }
            while (list[lo].compareTo(pivot) < 0 && lo < hi) {
                lo++;
            }
            if (lo < hi) {
                ModeData T = list[lo];
                list[lo] = list[hi];
                list[hi] = T;
            } else {
                return hi;
            }
        }
    }      
}


